//
// CODESCANNERS
// CodeScanners are smart pointers into a CodeBox that can be used
// to enumerate characters in the buffer and/or to do simple parsing. 
//
// This should logically be local to CodeBox, but the silly C# compiler won't let
// you store it in a separate file then.
//

using System;
using System.Globalization;

//global const
enum Error: int {
	SUCCESSFUL = 0,
	BUF_OUTOFBOUND = -1,
	BUF_BEGINBOUND = -3,
	BUF_ENDBOUND = -4
}

namespace Meta.Editor {

	public class CodeScanner {
		//
		// Constructor
		//
		public CodeScanner(int index, string[] l) {

			charIndex = index;
			lines = l;

			if (l.Length==0) {
				lineNumber=0;
				lineLength=0;
				charNumber=0;
			} else if (index >= 0) {
				int startOfLine=0;
				for (lineNumber=0; startOfLine<=charIndex; lineNumber++)
					startOfLine += lines[lineNumber].Length+1;
				lineNumber--;
				lineLength = lines[lineNumber].Length;
				startOfLine -= lineLength+1;
				charNumber = charIndex-startOfLine;
			} else {
				lineNumber = lineLength =0;
				charNumber = charIndex = -1;
			}
		}

		public CodeScanner(CodeScanner toCopy) {
			charIndex = toCopy.charIndex;
			charNumber = toCopy.charNumber;
			lineNumber = toCopy.lineNumber;
			lineLength = toCopy.lineLength;
			lines = toCopy.lines;
		}

		//
		// Fields
		//

		public string[] lines;
		// Line and character number of the next char to be returned.
		public int lineNumber;
		public int charNumber;
		public int charIndex;
		// Cached size of current line.
		public int lineLength;

		//
		// Events
		//
		public Meta.VoidThunk AdvancedToNewLine;

		//
		// Debugging properties
		// These aren't directly useful, but visual studio always displays the values of
		// all your properties every time you look at an object, so it gives a good
		// way of monitoring exactly what the stupid thing is doing
		//
		string DebugPosition {
			get {
				string currentLine = lines[lineNumber];

				if (charNumber<0)
					return "Newline before: "+currentLine;
				else
					return currentLine.Substring(0,charNumber)+"|"+currentLine.Substring(charNumber);
			}
		}


		//
		// Static stuff
		//

		public static System.Collections.Hashtable matchingBracket
			= new System.Collections.Hashtable();

		const char openDoubleQuote = '\u201c';
		const char closeDoubleQuote = '\u201d';
		const char openSingleQuote = '\u2018';
		const char closeSingleQuote = '\u2019';
		const char openSingleAngleBracket = '\u2039';
		const char closeSingleAngleBracket = '\u203a';
		const char openDoubleAngleBracket = '\u00ab';
		const char closeDoubleAngleBracket = '\u00bb';

		static char[] bracketedTokenOpeners = new char[] {openDoubleQuote, openSingleQuote, openDoubleAngleBracket};
		static char[] bracketedTokenClosers = new char[] {closeDoubleQuote, closeSingleQuote, closeDoubleAngleBracket};

		static void RegisterBrackets(char left, char right) {
			matchingBracket[left] = right;
			matchingBracket[right] = left;
		}

		static CodeScanner() {
			RegisterBrackets('(', ')');
			RegisterBrackets('[', ']');
			RegisterBrackets('{', '}');
			RegisterBrackets(openDoubleQuote, closeDoubleQuote);
			RegisterBrackets(openSingleQuote, closeSingleQuote);
			RegisterBrackets(openSingleAngleBracket, closeSingleAngleBracket);
			RegisterBrackets(openDoubleAngleBracket, closeDoubleAngleBracket);
			RegisterBrackets('\"', '\"');
		}

		static bool IsQuoteChar(char c) {
			return c=='\'' || c== '`' || c== ',' || c=='@';
		}

		//
		// CHARACTER ENUMERATION
		// Note: this doesn't use the standard .NET enumeration interface because it would
		// require boxing every character as we returned it.
		//

		public char CurrentChar {
			get {
				if (lineNumber==lines.Length||charNumber<0 || charNumber>= lineLength)
					return '\n';
				else
					return lines[lineNumber][charNumber];
			}
		}

		// move to the next character, returning the current character.
		public char Forward() {
			if (lineNumber==lines.Length)
				return '\n';
			charIndex++;
			if (charNumber<0) {
				charNumber=0;
				return '\n';
			} else if (lineNumber<0) {
				charNumber = lineNumber = charIndex = 0;
				lineLength = lines[lineNumber].Length;
				return '\n';
			} else if (charNumber>=lineLength) {
				lineNumber++;
				if (lineNumber<lines.Length)
					lineLength = lines[lineNumber].Length;
				charNumber = 0;

				// Used by indenter to detect new lines.
				if (AdvancedToNewLine!=null && !AtEndOfBuffer)
					// The end of the buffer isn't really a line, so signaling
					// the event here will misindent the last line
					AdvancedToNewLine();

				return '\n';
			} else {
				char ch = lines[lineNumber][charNumber];
				charNumber++;
				return ch;
			}
		}

		// Move to the previous character, returning the current character.
		public char Backward() {
			charIndex--;
			if (charNumber==lineLength) {
				charNumber--;
				return '\n';
			} else if (charNumber<0 || lineLength==0 || lineNumber==lines.Length) {
				lineNumber--;
				lineLength = lineNumber<0?0:lines[lineNumber].Length;
				charNumber = lineLength-1;
				return '\n';
			} else {
				char ch = lines[lineNumber][charNumber];
				charNumber--;
				return ch;
			}
		}


		// These could both be noticably optimized.
		public char BackwardSkippingEscapes() {
			char ch=' ';
			char maybePrefix = '\\';

			while (maybePrefix=='\\') {
				ch = Backward();
				maybePrefix = Backward();
			}
			Forward();
			return ch;
		}
		public char ForwardSkippingEscapes() {
			while (CurrentChar=='\\') {
				Forward();
				Forward();
			}
			return Forward();
		}


		//
		// Position predicates
		//

		public bool AtEndOfLine {
			get {
				return charNumber==lineLength;
			}
		}
		public bool AtBeginningOfLine {
			get {
				return charNumber==0;
			}
		}
		public bool AtEndOfBuffer {
			get {
				return lineNumber==lines.Length;
			}
		}
		public bool AtBeginningOfBuffer {
			get {
				return charIndex<0;
			}
		}
		public bool AtBeginningOfTopLevelRegion {
			get {
				// Check that either
				//    - We ran off the beginning of the buffer
				//    - We're on a line that starts with a prompt and has only whitespace from there to the current position
				//    - We're on a line that starts with a definition and the previous line is blank.
				if (charIndex<0)
					return true;

				if (lineNumber>0) {
					string prevLine = lines[lineNumber-1];
					if ( (prevLine.Length==0  // BlankLine
				  		    || (prevLine.EndsWith(CodeBox.endOfCommentString) 
						          && (prevLine.StartsWith(CodeBox.startOfCommentString)      // Ends a multiline comment
						                  || (prevLine.IndexOf(CodeBox.startOfCommentChar)<0)))
						    || prevLine.StartsWith(";"))   // Is a single-line comment
						 && (LookingAt("[define") || LookingAt("(define")))
						return true;
				}
				if (lineNumber>=lines.Length || lines[lineNumber].Length<2 || lines[lineNumber][0]!=Meta.TopLevel.promptCarret || charNumber<2)
					return false;
				for (int i=1; i<charNumber; i++)
					if (!Char.IsWhiteSpace(lines[lineNumber][i]))
						return false;
				return true;
			}
		}

		public bool AtPrompt {
			get {
				return CurrentChar==Meta.TopLevel.promptCarret && charNumber==0;
			}
		}

        // True if charIndex is at a blank line, including being outside the buffer
		public bool AtBlankLine {
			get {
				return lineNumber<0 || lineNumber>=lines.Length || lines[lineNumber].Length==0;
			}
		}

        // True if charIndex is inside the buffer and immediately after a blank line.
        public bool PrecededByBlankLine
        {
            get
            {
                return charNumber==0 && lineNumber>0 && lines[lineNumber].Length == 0;
            }
        }

        public bool NextLineIsDefinition
        {
			get {
				if (lineNumber>=lines.Length-1)
					// Last line
					return false;
				return lines[lineNumber+1].StartsWith("[define") || lines[lineNumber+1].StartsWith("(define");
			}
		}

		public bool IsDefinitionLine {
			get {
				return lineNumber>=0 && lineNumber<lines.Length && lines[lineNumber].Length>=4 && lines[lineNumber].StartsWith("[define");
			}
		}

		//
		// SCANNING OVER FULL EXPRESSIONS
		//
		//
		// Expression Scanning
		//
		public int SkipBackwardExpression() {
			return SkipBackwardExpression(true);
		}

		public int SkipBackwardExpression(bool leaveInBuffer) {
			int nestingLevel=0;

			// If we're at "[a |b", move to "[a| b" so the current character is the space.
			Backward();
			// Skip until we find something interesting
			SkipBackwardWhiteSpace();
			// Figure out what kind of expression we're looking at.
			if (char.GetUnicodeCategory(CurrentChar)==UnicodeCategory.ClosePunctuation) {
				if (System.Array.IndexOf(bracketedTokenClosers, CurrentChar)>=0) {
					// It's really a token
					Forward();
					SkipBackwardToken();
				} else {
					// It's a compound expression with brackets
					//Backward();
					while (!AtBeginningOfBuffer) {
						if (charIndex>0)
							SkipBackwardWhiteSpace();
						switch (Char.GetUnicodeCategory(CurrentChar)) {
							case UnicodeCategory.OpenPunctuation:
								nestingLevel--;
								if (nestingLevel<=0) {
									if (charIndex>0)
										Backward();
									goto done;
								}
								break;
							case UnicodeCategory.ClosePunctuation:
								nestingLevel++;
								break;
							default:
								SkipBackwardToken();
								break;
						}
						Backward();
					}
				}
			done:
				// We just went past it, so go forward again.
				if (AtBeginningOfBuffer) {
					if (leaveInBuffer) {
						charIndex=0;
						lineNumber=0;
						charNumber=0;
					}
				} else {
					if (charIndex>0 && !IsQuoteChar(CurrentChar))
						Forward();
				}
				return charIndex;
			} else {
				// It's an atomic expression.

				// We're looking at the last character of it.  If it just happens to be one character long,
				// we're screwed, so unskip the last character.
				Forward();
				SkipBackwardToken();
				return charIndex;
			}
		}

		// After call, current char is [ of enclosing expression.
		public int SkipBackwardToEnclosingExpression() {
			int saved = charIndex;
			return MaybeSkipBackwardToEnclosingExpression()?charIndex:saved;
		}
		public bool MaybeSkipBackwardToEnclosingExpression() {
			if (AtPrompt)
				return false;
			if (Char.GetUnicodeCategory(CurrentChar)==UnicodeCategory.OpenPunctuation)
				Backward();
			do {
				SkipBackwardExpression();
				Backward();
				if (Char.GetUnicodeCategory(CurrentChar)==UnicodeCategory.OpenPunctuation)
					break;
				SkipBackwardWhiteSpace();
				if (charNumber==0 && lines[lineNumber][0]==Meta.TopLevel.promptCarret)
					return false;
				if (Char.GetUnicodeCategory(CurrentChar)==UnicodeCategory.OpenPunctuation)
					break;
				Forward();
			} while (!AtBeginningOfTopLevelRegion);

			return !AtBeginningOfTopLevelRegion&& Char.GetUnicodeCategory(CurrentChar)==UnicodeCategory.OpenPunctuation;
		}

		public int SkipBackwardToTopLevelExpression(bool blankLinesMarkTopLevelRegions) {
			CodeScanner saved=(CodeScanner)this.MemberwiseClone();
			CodeScanner temp;
			int count=0;

			if (AtBeginningOfTopLevelRegion)
				return charIndex;

#if broken
			// This causes it to screw up when you hit return inside an expression
			if (Editor.blankLinesMarkTopLevelRegions && AtBlankLine) {
				SkipBackwardExpression();
				return charIndex;
			}
#endif

            if (blankLinesMarkTopLevelRegions && PrecededByBlankLine)
            {
                SkipBackwardExpression();
                return charIndex;
            }

			do {
				temp=(CodeScanner)this.MemberwiseClone();
				count++;
			} while (MaybeSkipBackwardToEnclosingExpression());

			if (count==1 && AtBeginningOfBuffer) {
				// Ran off the end of the buffer.
				// We must be at the end of a top-level expression.
				charIndex=saved.charIndex;
				charNumber=saved.charNumber;
				lineNumber=saved.lineNumber;
				lineLength=saved.lineLength;
				if (Char.GetUnicodeCategory(CurrentChar)!=UnicodeCategory.OpenPunctuation)
					SkipBackwardExpression();
			}
			
			if (charIndex<0)
				Forward();

			if (AtBeginningOfTopLevelRegion)
				return charIndex;

			if (AtPrompt) {
				SkipForwardExpression();
				SkipForwardWhiteSpace();
				return charIndex;
			}

			if (count>1) {
				charIndex=temp.charIndex;
				charNumber=temp.charNumber;
				lineNumber=temp.lineNumber;
				lineLength=temp.lineLength;
			} 
			return charIndex;
		}

		public int SkipForwardExpression() {
			int nestingLevel=0;

			SkipForwardWhiteSpace();
			while (IsQuoteChar(CurrentChar)) {
				// It's a quoted expression.
				Forward();
			}
			SkipForwardWhiteSpace();
			if (char.GetUnicodeCategory(CurrentChar)==UnicodeCategory.OpenPunctuation) {
				if (System.Array.IndexOf(bracketedTokenOpeners, CurrentChar)>=0) {
					// It's really a token
					SkipForwardToken();
				} else {
	
					while (!AtEndOfBuffer) {
						switch (Char.GetUnicodeCategory(Forward())) {
							case UnicodeCategory.ClosePunctuation:
								nestingLevel--;
								if (nestingLevel<=0)
									goto done;
								break;
							case UnicodeCategory.OpenPunctuation:
								nestingLevel++;
								break;
						}
					}
				}
			done:
				return charIndex;
			} else {
				// It's a nomal expression.
				SkipForwardToken();
				return charIndex;
			}
		}


		// After call, current character is last non-whitespace char at or before previous position
		public int SkipBackwardWhiteSpace() {
			while (true) {
				while (!AtBeginningOfBuffer && char.IsWhiteSpace(Backward()));
				// Unskip the non-whitespace character.
				if (!AtBeginningOfBuffer)
					Forward();
				if (CurrentChar==CodeBox.endOfCommentChar)
					while (!AtBeginningOfBuffer && Backward()!=CodeBox.startOfCommentChar);
				else
					break;
			}
			return charIndex;
		}

		// After call, current character is first non-whitespace char after previous position
		public int SkipForwardWhiteSpace() {
			while (true) {
				while (!AtEndOfBuffer && char.IsWhiteSpace(Forward()));
				// Unskip the non-whitespace character.
				if (!AtEndOfBuffer)
					Backward();
				if (CurrentChar==CodeBox.startOfCommentChar)
					while (!AtEndOfBuffer && Forward()!=CodeBox.endOfCommentChar);
				else
					break;
			}
			return charIndex;
		}

		// After call, current character is first non-whitespace char after previous position OR the last char of the line
		public int SkipForwardWhiteSpaceToEOL() {
			while (!AtEndOfBuffer & !AtEndOfLine & char.IsWhiteSpace(Forward()));
			// Unskip the non-whitespace character.
			if (!AtBeginningOfBuffer)
				Backward();
			return charIndex;
		}

		// After call, current character is on the first character of the following line
		public int SkipForwardToBOL() {
			do
				Forward();
			while (!AtEndOfBuffer&&!AtBeginningOfLine);
			return charIndex;
		}

		// After call, current char is last whitespace before previous position.
		public int SkipBackwardToWhiteSpace() {
			while (!AtBeginningOfBuffer && !char.IsWhiteSpace(Backward()));
			// Unskip the non-whitespace character.
			Forward();
			return charIndex;
		}

		// After call, current char is the first whitespace before previous position.
		public int SkipForwardToWhiteSpace() {
			while (!AtEndOfBuffer && !char.IsWhiteSpace(Forward()));
			// Unskip the non-whitespace character.
			Backward();
			return charIndex;
		}

		// After call, current char is the first character of token before prev position.
		public int SkipBackwardToken() {
			char ch;

			SkipBackwardWhiteSpace();
			if (AtBeginningOfBuffer)
				return charIndex;
			ch = Backward(); // Get the last char of the token
			if (AtBeginningOfBuffer)
				return charIndex;
			UnicodeCategory category = char.GetUnicodeCategory(ch);
			if (//category==UnicodeCategory.ClosePunctuation || 
				category==UnicodeCategory.FinalQuotePunctuation) {
				char matching = (char)matchingBracket[ch];

				while (!AtBeginningOfBuffer && BackwardSkippingEscapes()!=matching);
				Forward();
			} else {
				UnicodeCategory cat;
				do {
					ch = Backward();
					cat = char.GetUnicodeCategory(ch);
				} while (!AtBeginningOfBuffer && !char.IsWhiteSpace(ch)
					&& cat!=UnicodeCategory.OpenPunctuation && cat!=UnicodeCategory.ClosePunctuation);
				if (!AtBeginningOfBuffer || cat==UnicodeCategory.OpenPunctuation)
					Forward();                  // unskip whitespace or punctuation
				Forward();                      // unskip first character of token
			}
			return charIndex;
		}

		// After call, current char is first char after token.
		public int SkipForwardToken() {
			SkipForwardWhiteSpace();

			char ch = Forward();
			if (char.GetUnicodeCategory(ch)==UnicodeCategory.InitialQuotePunctuation) {
				// It's a quoted expression
				char matching = (char)CodeScanner.matchingBracket[ch];
				do {
					ch = Forward();
					if (ch=='\\')
						ch = Forward();
				} while (!AtEndOfBuffer && ch != matching);
			} else {
				// It's atomic
				do {
					ch = Forward();
				} while (!AtEndOfBuffer && !char.IsWhiteSpace(ch)
					&& char.GetUnicodeCategory(ch)!=UnicodeCategory.ClosePunctuation);
				if (!AtEndOfBuffer)
					Backward(); // unskip whitespace or punctuation
			}
			return charIndex;
		}

		public int SkipBackwardToBOL() {
			charIndex -= charNumber<0?lineLength:charNumber;
			charNumber=0;
			return charIndex;
		}

		public char FindCorrectQuoteMark(char openQuote) {
			char closeQuote = (char)matchingBracket[openQuote];
			char open;
			int position = charIndex;

			SkipBackwardToBOL();
			while (true) {
				// Maybe find a complex token
				for (open = Forward();
					charIndex < position 
					&& char.GetUnicodeCategory(open)!=UnicodeCategory.InitialQuotePunctuation;
					open = Forward());
				if (charIndex>=position)
					// Not in a token
					return open==openQuote?closeQuote:openQuote;
				// We're in a token.  Scan for the end.
				char close = (char)matchingBracket[open];
				for (char ch=ForwardSkippingEscapes(); ch!=close; ch=ForwardSkippingEscapes())
					if (charIndex>=position)
						// It was in the token
						return open==openQuote?closeQuote:openQuote;
				// Keep going
			}
		}

		//Rachel's special code scanner thingeys

		//After call, current char is the first whitespace or newline before 
		//the prev position
		public int SkipForwardToWordSeparator() {
			char nextChar;
			while (!AtEndOfBuffer &&
				!char.IsWhiteSpace(nextChar = Forward()) &&
				"[](){}".IndexOf(nextChar)<0 &&
				!char.IsSeparator(nextChar) ) {}
			if (AtEndOfBuffer || AtBeginningOfBuffer)
				return (int) Error.BUF_OUTOFBOUND;
			else {
				Backward();
				return charIndex;
			}
		}

		public int SkipBackwardToWordSeparator() {
			char PreviousChar;
			while (!AtBeginningOfBuffer &&
				!char.IsWhiteSpace(PreviousChar = Backward()) &&
				"[](){}".IndexOf(PreviousChar)<0 &&
				!char.IsSeparator(PreviousChar) ) {}
			if (AtEndOfBuffer || AtBeginningOfBuffer)
				return (int) Error.BUF_OUTOFBOUND;
			else {
				Forward();
				return charIndex;
			}
		}

		//after call, current char is the first token before prev position
		public int SkipForwardToChar(char tok) {
			while (!AtEndOfBuffer && !char.Equals(tok, Forward()));
			// Unskip the non-token character.
			if (AtEndOfBuffer || AtBeginningOfBuffer) 
				return (int) Error.BUF_OUTOFBOUND;
			else {
				Backward();
				return charIndex;
			}
		}

		public int SkipForwardToChar(char[] toks) {
			char NextChar;
			bool FoundTok = false;
			while(!AtEndOfBuffer && !FoundTok) {
				NextChar = Forward();
				foreach (char tok in toks) {
					if (char.Equals(NextChar, tok)) 
						FoundTok = true;
				}
			}
			if (AtEndOfBuffer || AtBeginningOfBuffer)
				return (int) Error.BUF_OUTOFBOUND;
			else {
				Backward();
				return charIndex;
			}
		}

		public int SkipBackwardToToken(char tok) {
			while (!AtBeginningOfBuffer && !char.Equals(tok, Backward()));
			// Unskip the non-token character.
			if (AtBeginningOfBuffer || AtEndOfBuffer) 
				return (int) Error.BUF_OUTOFBOUND;
			else {
				Forward();
				return charIndex;
			}
		}

		public int SkipBackwardToToken(char[] toks) {
			char PreviousChar;
			bool FoundTok = false;
			while(!AtBeginningOfBuffer && !FoundTok) {
				PreviousChar = Backward();
				foreach (char tok in toks) {
					if (char.Equals(PreviousChar, tok)) 
						FoundTok = true;
				}
			}
			if (AtEndOfBuffer || AtBeginningOfBuffer)
				return (int) Error.BUF_OUTOFBOUND;
			else {
				return charIndex;
			}
		}

		// Return true if the current position in the buffer is the start of the string STR.
		public bool LookingAt(string str) {
			if (lineNumber>=lines.Length || charNumber<0)
				return false;				// at end of line
			string line = lines[lineNumber];
			if (line.Length<charNumber+str.Length)
				return false;				// not enough room in line for str
			return line.Substring(charNumber, str.Length)==str;
		}
	}
}